\chapter{lambda表达式扩展}\label{ch6}
C++11引入的lambda和C++14引入的泛型lambda是一个很大的成功。
它允许我们将函数作为参数传递，这让我们能更轻易的指明一种行为。

C++17扩展了lambda表达式的应用场景：
\begin{itemize}
    \item 在常量表达式中使用（也就是在编译期间使用）
    \item 在需要当前对象的拷贝时使用（例如，当在不同的线程中调用lambda时）
\end{itemize}


\section{\texttt{constexpr} lambda}
自从C++17起，lambda表达式会尽可能的隐式声明\texttt{constexpr}。
也就是说，任何只使用有效的编译期上下文
（例如，只有字面量，没有静态变量，没有虚函数，没有\texttt{try/catch}，
没有\texttt{new/delete}的上下文）的lambda都可以被用于编译期。

例如，你可以使用一个lambda表达式计算参数的平方，并将计算结果用作\texttt{std::array<>}的大小，
即使这是一个编译期的参数：
\begin{lstlisting}
    auto squared = [](auto val) {   // 自从C++17起隐式constexpr
        return val*val;
    };
    std::array<int, squared(5)> a;  // 自从C++17起OK => std::array<int, 25>
\end{lstlisting}
使用编译期上下文中不允许的特性将会使lambda失去成为\texttt{constexpr}的能力，
不过你仍然可以在运行时上下文中使用lambda：
\begin{lstlisting}
    auto squared2 = [](auto val) {  // 自从C++17起隐式constexpr
        static int calls = 0;       // OK，但会使该lambda不能成为constexpr
        ...
        return val*val;
    };
    std::array<int, squared2(5)> a;     // ERROR：在编译期上下文中使用了静态变量
    std::cout << squared2(5) << '\n';   // OK
\end{lstlisting}
为了确定一个lambda是否能用于编译期，你可以将它声明为\texttt{constexpr}：
\begin{lstlisting}
    auto squared3 = [](auto val) constexpr {  // 自从C++17起OK
        return val*val;
    };
\end{lstlisting}
如果指明返回类型的话，语法看起来像下面这样：
\begin{lstlisting}
    auto squared3i = [](int val) constexpr -> int {  // 自从C++17起OK
        return val*val;
    };
\end{lstlisting}
关于\texttt{constexpr}函数的规则也适用于lambda：如果一个lambda在运行时上下文中使用，
那么相应的函数体也会在运行时才会执行。

然而，如果在声明了\texttt{constexpr}的lambda内使用了编译期上下文中不允许的特性
将会导致编译错误：\footnote{不允许出现在编译期上下文中的特性有：静态变量、虚函数、
\texttt{try/catch}、\texttt{new/delete}等。}
\begin{lstlisting}
    auto squared4 = [](auto val) constexpr {
        static int calls = 0;  // ERROR：在编译期上下文中使用了静态变量
        ...
        return val*val;
    };
\end{lstlisting}
一个隐式或显式的\texttt{constexpr} lambda的函数调用符也是\texttt{constexpr}。
也就是说，如下定义：
\begin{lstlisting}
    auto squared = [](auto val) {   // 从C++17起隐式constexpr
        return val*val;
    };
\end{lstlisting}
将会被转换为如下\emph{闭包类型(closure type)}：
\begin{lstlisting}
    class CompilerSpecificName {
    public:
        ...
        template<typename T>
        constexpr auto operator() (T val) const {
            return val*val;
        }
    };
\end{lstlisting}
注意，这里生成的闭包类型的函数调用运算符自动声明为\texttt{constexpr}。
自从C++17起，如果lambda被显式或隐式地定义为\texttt{constexpr}，
那么生成的函数调用运算符将自动是\texttt{constexpr}。

注意如下定义：
\begin{lstlisting}
    auto squared1 = [](auto val) constexpr {  // 编译期lambda调用
        return val*val;
    };
\end{lstlisting}
和如下定义：
\begin{lstlisting}
    constexpr auto squared2 = [](auto val) {  // 编译期初始化squared2
        return val*val;
    };
\end{lstlisting}
是不同的。

第一个例子中如果（只有）lambda是\texttt{constexpr}那么它可以被用于编译期，
但是\texttt{squared1}可能直到运行期才会被初始化，
这意味着如果静态初始化顺序很重要那么可能导致问题
（例如，可能会导致\emph{static initialization order fiasco}）。
如果用lambda初始化的闭包对象是\texttt{constexpr}，那么该对象将在程序开始时就初始化，
但lambda可能还是只能在运行时使用。因此，可以考虑使用如下定义：
\begin{lstlisting}
    constexpr auto squared = [](auto val) constexpr {
        return val*val;
    };
\end{lstlisting}

\subsection{使用\texttt{constexpr} lambda}
这里有一个使用\texttt{constexpr} lambda的例子。
假设我们有一个字符序列的哈希函数，这个函数迭代字符串的每一个字符反复更新哈希值：
\footnote{djb2算法的源码见\url{http://www.cse.yorku.ca/~oz/hash.html}。}
\begin{lstlisting}
    auto hashed = [](const char* str) {
        std::size_t hash = 5381;        // 初始化哈希值
        while (*str != '\0') {
            hash = hash * 33 ^ *str++;  // 根据下一个字符更新哈希值
        }
        return hash;
    };
\end{lstlisting}
使用这个lambda，我们可以在编译期初始化不同字符串的哈希值，并定义为枚举：
\begin{lstlisting}
    enum Hashed { beer = hashed("beer"),
                  wine = hashed("wine"),
                  water = hashed("water"), ... };   // OK，编译期哈希
\end{lstlisting}
我们也可以在编译期计算\texttt{case}标签：
\begin{lstlisting}
    switch (hashed(argv[1])) {  // 运行时哈希
        case hashed("beer"):    // OK，编译期哈希
            ...
            break;
        case hashed("wine"):
            ...
            break;
        ...
    }
\end{lstlisting}
注意，这里我们将在编译期调用\texttt{case}标签里的\texttt{hashed}，
而在运行期间调用\texttt{switch}表达式里的\texttt{hashed}。

如果我们使用编译期lambda初始化一个容器，那么编译器优化时很可能在编译期就计算出容器的初始值
（这里使用了\hyperref[ch9.2.6.3]{\texttt{std::array}的类模板参数推导}）：
\begin{lstlisting}
    std::array arr{ hashed("beer"),
                    hashed("wine"),
                    hashed("water") };
\end{lstlisting}
你甚至可以在\texttt{hashed}函数里联合使用另一个\texttt{constexpr} lambda。
设想我们把\texttt{hashed}里根据当前哈希值和下一个字符值更新哈希值的逻辑定义为一个参数：
\begin{lstlisting}
    auto hashed = [](const char* str, auto combine) {
        std::size_t hash = 5381;            // 初始化哈希值
        while (*str != '\0') {
            hash = combine(hash, *str++);   // 用下一个字符更新哈希值
        }
        return hash;
    };
\end{lstlisting}
这个lambda可以像下面这样使用：
\begin{lstlisting}
    constexpr std::size_t hv1{hashed("wine", [](auto h, char c) {return h*33 + c;})};
    constexpr std::size_t hv2{hashed("wine", [](auto h, char c) {return h*33 ^ c;})};
\end{lstlisting}
这里，我们在编译期通过改变更新逻辑初始化了两个不同的\texttt{"wine"}的哈希值。
两个\texttt{hashed}都是在编译期调用。


\section{向lambda传递\texttt{this}的拷贝}
当在非静态成员函数里使用lambda时，你不能隐式获取对该对象成员的使用权。
也就是说，如果你不捕获\texttt{this}的话你将不能在lambda里使用该对象的任何成员
（即使你用\texttt{this->}来访问也不行）：
\begin{lstlisting}
    class C {
    private:
        std::string name;
    public:
        ...
        void foo() {
            auto l1 = [] { std::cout << name << '\n'; };        // ERROR
            auto l2 = [] { std::cout << this->name << '\n'; };  // ERROR
            ...
        }
    };
\end{lstlisting}
在C++11和C++14里，你可以通过值或引用捕获\texttt{this}：
\begin{lstlisting}
    class C {
    private:
        std::string name;
    public:
        ...
        void foo() {
            auto l1 = [this] { std::cout << name << '\n'; };    // OK
            auto l2 = [=] { std::cout << name << '\n'; };       // OK
            auto l3 = [&] { std::cout << name << '\n'; };       // OK
            ...
        }
    };
\end{lstlisting}
然而，问题是即使是用拷贝的方式捕获\texttt{this}实质上获得的也是引用
（因为只会拷贝\texttt{this}\emph{指针}）。当lambda的生命周期比该对象的生命周期更长的时候，
调用这样的函数就可能导致问题。比如一个极端的例子是在lambda中开启一个新的线程来完成某些任务,
调用新线程时正确的做法是传递整个对象的拷贝来避免并发和生存周期的问题，而不是传递该对象的引用。
另外有时候你可能只是简单的想向lambda传递当前对象的拷贝。

自从C++14起有了一个解决方案，但可读性和实际效果都比较差：
\begin{lstlisting}
    class C {
    private:
        std::string name;
    public:
        ...
        void foo() {
            auto l1 = [thisCopy=*this] { std::cout << thisCopy.name << '\n'; };
            ...
        }
    };
\end{lstlisting}
例如，当使用了\texttt{=}或者\texttt{\&}捕获了其他对象的时候你可能会在不经意间使用\texttt{this}：
\begin{lstlisting}
    auto l1 = [&, thisCopy=*this] {
        thisCopy.name = "new name";
        std::cout << name << '\n'; // OOPS：仍然使用了原来的name
    };
\end{lstlisting}
自从C++17起，你可以通过\texttt{*this}显式地捕获当前对象的拷贝：
\begin{lstlisting}
    class C {
    private:
        std::string name;
    public:
        ...
        void foo() {
            auto l1 = [*this] { std::cout << name << '\n'; };
            ...
        }
    };
\end{lstlisting}
这里，捕获\texttt{*this}意味着该lambda生成的闭包将存储当前对象的一份\emph{拷贝}。

你仍然可以在捕获\texttt{*this}的同时捕获其他对象，只要没有多个\texttt{this}的矛盾：
\begin{lstlisting}
    auto l2 = [&, *this] { ... };       // OK
    auto l3 = [this, *this] { ... };    // ERROR
\end{lstlisting}
这里有一个完整的例子：
\inputcodefile{lang/lambdathis.cpp}
lambda里捕获了\texttt{*this}，所以传递进lambda的是一份拷贝。
因此，即使在\texttt{d}被销毁之后使用捕获的对象也没有问题。

如果我们使用\texttt{[this]}、\texttt{[=]}或者\texttt{[\&]}捕获\texttt{this}，
那么新线程将会陷入未定义行为，因为当线程中打印\texttt{name}的时候将会使用一个已经销毁的
对象的成员。


\section{以常量引用捕获}
通过使用一个新的库工具，现在也可以\nameref{ch25.2.1}。


\section{后记}
\texttt{constexpr} lambda由Faisal Vali、Ville Voutilainen和Gabriel Dos Reis在
\url{https://wg21.link/n4487}中首次提出。
最终被接受的是Faisal Vali、Jens Maurer、Richard Smith发表
于\url{https://wg21.link/p0170r1}的提案。

在lambda中捕获\texttt{*this}由H. Carter Edwards、Christian Trott、Hal Finkel、
Jim Reus、Robin Maffeo、Ben Sander\\
在\url{https://wg21.link/p0018r0}中首次提出。
最终被接受的是H. Carter Edwards、Daveed Vandevoorde、Christian Trott、Hal Finkel、
Jim Reus、Robin Maffeo、Ben Sander发表于\url{https://wg21.link/p0180r3}的提案。